<?php
namespace TwinPeaks\Core;

class CRule
{
	use TDecorator, TAppInjector;
	
	/**
	 * 
	 */
	const COND_TYPE_STRING = 1;
	const COND_TYPE_SUBSTR = 2;
	const COND_TYPE_REGEXP = 3;
	const COND_TYPE_CALLBACK = 4;

	/**
	 * @var string Default controller class
	 */
	static private $defaultCtrl;
	
	/**
	 * @var
	 */
	private $ruleId;
	
	private $ctrlInstance;
	
	/**
	 * @var string 
	 */
	private $ctrlClassName;
	
	/**
	 * @var string
	 */
	private $ctrlMethodName;
	
	private $cond;
	private $condRegExpModifiers = '';
	private $condParams = [];
	private $condType;
	
	private $URI;
	private $URIGetter;
	
	/**
	 * @var boolean
	 */
	private $isAuthReqFlag;
	
	/**
	 * @var array URI path aliases
	 */
	private $pathAliases = [];
	
	/**
	 * @var integer
	 */
	private $allowedMethods;

	/**
	 * @var integer
	 */
	static private $defaultAllowedMethods;

	/**
	 * @var string
	 */
	static private $URI_pathPrefix = '';
	
	/**
	 * @var array
	 */
	private $filterParams = [];
	
	/**
	 * @var mixed
	 */
	private $executionResult;

	/**
	 * 
	 */
	private function __construct($condition, $ctrlMethodPair, $ruleId)
	{		
		// By default GET and POST request methods are allowed
		$this->allowedMethods = self::getDefaultAllowedMethods();

		$this->cond = $condition;		
		$this->URI = Http\CUri::createFromString('/');		
		$this->ruleId = $ruleId;
		
		$parts = explode('::', $ctrlMethodPair);
		if (count($parts) == 1) // Default controller name will be used
		{
			$this->ctrlClassName = $this->normalizeClassName(self::$defaultCtrl);
			$this->ctrlMethodName = $parts[0];
		}
		elseif (count($parts) == 2)
		{
			$this->ctrlClassName = $this->normalizeClassName($parts[0]);
			$this->ctrlMethodName = $parts[1];
		}		
	}
	
	/**
	 * @return void
	 */
	final function getExecutionResult()
	{
		return $this->executionResult;
	}
	
	/**
	 * @return void
	 */
	final function setExecutionResult($value)
	{
		$this->executionResult = $value;
	}
	
	/**
	 * @return integer
	 */
	final static function getDefaultAllowedMethods()
	{
		if (isset(self::$defaultAllowedMethods))
			return self::$defaultAllowedMethods;
		else
			return ( Http\GET_METHOD | Http\POST_METHOD );
	}
	
	/**
	 * @return void 
	 */
	final static function setDefaultAllowedMethods($bitField)
	{
		self::$defaultAllowedMethods = $bitField;
	}
	
	/**
	 * @return void
	 */
	final static function setURI_pathPrefix($prefix)
	{
		self::$URI_pathPrefix = $prefix;
	}
	
	/**
	 * @return string
	 */
	final static function getURI_pathPrefix()
	{
		return self::$URI_pathPrefix;
	}
	
	/**
	 * TRUE if only authorized users are allowed to perform rule
	 * 
	 * @return boolean
	 */
	final function isAuthReq()
	{
		return $this->isAuthReqFlag;
	}

	/**
	 * @return CRule
	 */
	final function setAuthReq($flagValue = true)
	{
		$this->isAuthReqFlag = $flagValue;
		return $this;
	}
	
	/**
	 *
	 */
	function checkUserRights()
	{
		$self = Core\get_class_method_name(__METHOD__);
		if ($this->hasDecoration($self))
			return $this->renderDecoration();
	}

	/**
	 * @return CRule
	 */
	static final function matchStr($URI_Path, $ctrlMethodPair, $ruleId = null)
	{
		$rule = new static($URI_Path, $ctrlMethodPair, $ruleId);
		
		$prefix = self::getURI_pathPrefix();
		$rule->cond = $prefix . $rule->cond;
		$rule->condType = self::COND_TYPE_STRING;
		$rule->getURI()->setPath($prefix . $URI_Path);
		return $rule;
	}
	
	/**
	 * @return CRule
	 */
	static final function matchSubstr($URI_Path, $ctrlMethodPair, $ruleId = null)
	{
		$rule = new static($URI_Path, $ctrlMethodPair, $ruleId);
		$rule->condType = self::COND_TYPE_SUBSTR;
		return $rule;
	}

	/**
	 * @return CRule
	 */
	static final function matchRegexp($URIPathRegexp, $ctrlMethodPair, $ruleId = null)
	{
		if (is_array($URIPathRegexp))
		{
			$URIPathRegexp = $URIPathRegexp[0];
			$this->condRegExpModifiers = $URIPathRegexp[1];
		}
		$rule = new static($URIPathRegexp, $ctrlMethodPair, $ruleId);
		$rule->condType = self::COND_TYPE_REGEXP;
		return $rule;
	}
	
	/**
	 * @return CRule
	 */
	static final function matchCallback($callback, $ctrlMethodPair, $ruleId)
	{
		$rule = new static($callback, $ctrlMethodPair, $ruleId);
		$rule->condType = self::COND_TYPE_CALLBACK;
		return $rule;
	}
	
	/**
	 * @return integer
	 */
	function getId()
	{
		return $this->ruleId;
	}
	
	/**
	 * @return array
	 */
	final function getAllowedMethods()
	{
		return $this->allowedMethods;
	}
	
	/**
	 * @return CRule
	 */
	final function setAllowedMethods($bitField)
	{
		$this->allowedMethods = $bitField;
	}
	
	/**
	 * @return boolean
	 */
	final function isMethodAllowed($methodType)
	{
		return (boolean)($this->allowedMethods & $methodType);
	}
	
	/**
	 * @return CRule
	 */
	function setURIGetter($callback)
	{
		$this->URIGetter = $callback->bindTo($this, get_called_class());		
		return $this;
	}
	
	/**
	 * @return CRule
	 */
	function setURI($uriMixed)
	{
		if (is_string($uriMixed))
			$this->URI = Http\CUri::createFromString($uriMixed);
		elseif ($uriMixed instanceof Http\CUri)
			$this->URI = $uriMixed;
		
		return $this;
	}
	
	/**
	 * @return CUri
	 */
	function getURI()
	{
		if (isset($this->URIGetter))
		{
			$args = func_get_args();
			return call_user_func_array($this->URIGetter, $args);
		}
		else
			return $this->URI;
 	}
		
	/**
	 * @return CRule
	 */
	function setPathAlias($URI_path)
	{
		$this->pathAliases[] = $URI_path;
		return $this;
	}
	
	/**
	 * @return CRule
	 */
	function setPathAliasParams()
	{
		$args = func_get_args();
		$this->setPathAlias($args[0]);
		$this->condParams = array_slice($args, 1);
		return $this;
	}
	
	/**
	 * @return boolean
	 */
	function checkCondition(Http\CUri $uri)
	{
		$path = $uri->getPath();
		foreach ($this->pathAliases as $alias)
			if (!strcmp($path, $alias))
			{
				$this->getURI()->setPath($alias);
				return true;
			}
		switch ($this->condType)
		{
			case self::COND_TYPE_STRING:
				if (!strcmp($path, $this->cond))
					return true;
				else
					return false;
			
			case self::COND_TYPE_SUBSTR:
				if (strpos($path, $this->cond) !== false)
					return true;
				else
					return false;
			
			case self::COND_TYPE_REGEXP:
				$matches = null;
				$regExp = '|' . $this->cond . '|' . $this->condRegExpModifiers;
				$result = preg_match($regExp, $path, $matches);
				if ($result)
					$this->condParams = array_slice($matches, 1);				
				return $result;
				
			case self::COND_TYPE_CALLBACK:
				$result = call_user_func($this->cond, $uri);

			default:
				return false;
		}
	}
	
	/**
	 * @return array
	 */
	function getConditionParams()
	{
		return $this->condParams;
	}
	
	/**
	 * @return string
	 */
	function getControllerMethodName()
	{
		return $this->ctrlMethodName;
	}
	
	/**
	 * @return CController
	 */
	final function getController()
	{
		return $this->ctrlInstance;
	}
	
	/**
	 * @return CRule
	 */
	function filterAddParam(CFilterParam $param)
	{
		$this->filterParams[] = $param;
		return $this;
	}
	
	/**
	 * @return array 
	 */
	final function execute()
	{
		$app = self::getApplication();
		
		$this->ctrlInstance = new $this->ctrlClassName(
			$this->getControllerMethodName(), $this->getConditionParams());
		$fc = $app->getFrontController();
		$this->ctrlInstance->setFrontController($fc);
		
		$filter = $fc->getFilter();
		if ($filter)
			foreach ($this->filterParams as $param)
				$filter->addParam($param);
		
		$cascadeArgs = [$this];
		cascade_invoc_no_break($app->getCascade(), 'preRuleExecution', $cascadeArgs);
		$this->setExecutionResult($this->ctrlInstance->execute());
		
		if ($app->getOption(OPT_RETURN_CONTROLLER_RESULT))
			cascade_invoc_no_break([$this->ctrlInstance], 'preRuleExecution', $cascadeArgs);			
		else
			cascade_invoc_no_break($app->getReverseCascade(), 'postRuleExecution', $cascadeArgs);
	}
	
	/**
	 * @access public
	 * @return void
	 */
	final static function setDefaultController($ctrlClassName)
	{
		self::$defaultCtrl = $ctrlClassName;
	}
	
	/**
	 * @return string
	 */
	private function normalizeClassName($className)
	{
		if (isFQCN($className))
			return $className;
		else
			return self::getApplication()->getAppNamespace() . '\\' . $className;	
	}
}
